Pro ASP.NET Core MVC2(第7版)翻译

第31章:模型约定和 Action 约束

作者:Adam Freeman
翻译:陈广
日期:2018-11-2


在这本书的整个过程中,我都强调 MVC 开发没有什么神奇之处,一个小小的幕后窥探就会揭示出所有东西是如何结合在一起来交付我在前几章中描述的特性的。

在本书的最后一章中,我描述了两个有用的特性,它们使您可以定制 MVC 应用程序的工作方式。模型约定允许您替换用于创建控制器和 action 的约定,覆盖默认应用的控制器和 action。Action 约束允许您指定 action 可用于何种类型的请求,这为 MVC 在选择处理请求的 action 时提供了指导。

如果您愿意,可以跳过本章(而且您可能希望跳过这一章,因为某些地方比较难懂),但是下次应用程序出错时请记住这一章。您不需要经常使用我在本章中描述的特性 —— 甚至根本不需要 —— 但是了解 MVC 工作原理的知识越多,就越能在出现问题时处理这些问题。表31-1为本章概述。

表 31-1:本章概述

问题 解决方案 清单
自定义应用程序模型 使用某一个内置特性或创建一个自定义模型约定 1-14
在整个应用程序中进行定制 定义一个全局模型约定 15,16
区分可以处理请求的两个 action 方法 使用 action 约束 17-25

准备示例项目

在本章中,我使用【ASP.NET Core Web 应用程序(.net core)】模板创建了一个名为 ConventionsAndConstraints 的新的空项目。清单31-1显示了Startup类,它设置了 MVC 框架和对开发有用的中间件组件。

清单 31-1:ConventionsAndConstraints 文件夹下的 Startup.cs 文件的内容

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace ConventionsAndConstraints
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

创建视图模型、控制器和视图

对于本章中的许多示例来说,知道使用哪种方法来响应请求是很有帮助的。为此,我创建了一个 Models 文件夹,并在其中添加了一个名为 Result.cs 的类文件,用于定义清单31-2中所示的类。该类将允许本章中的控制器将有关请求处理方式的信息传递给视图。

清单 31-2:Models 文件夹下的 Result.cs 文件的内容

namespace ConventionsAndConstraints.Models
{
    public class Result
    {
        public string Controller { get; set; }
        public string Action { get; set; }
    }
}

对于本章,我只需要一个控制器和视图。我创建了 Controllers 文件夹,添加了一个名为 HomeController.cs 的类文件,并使用它来定义清单31-3所示的类。 清单 31-3:Controllers 文件夹下的 HomeController.cs 文件的内容

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;

namespace ConventionsAndConstraints.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

这个控制器中的两个 action 方法都渲染一个名为 Result 的视图,我通过创建 Views/Home 文件夹和添加一个视图文件(如清单31-4所示的标记)来定义该视图。

清单 31-4:Views/Home 文件夹下的 Result.cshtml 文件的内容

@model Result
@{ Layout = null; }

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <link href="/lib/twitter-bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <title>Result</title>
</head>
<body class="m-1 p-1">
    <table class="table table-sm table-bordered">
        <tr><th>Controller:</th><td>@Model.Controller</td></tr>
        <tr><th>Action:</th><td>@Model.Action</td></tr>
    </table>
</body>
</html>

视图依赖于 Bootstrap CSS 包来设置 HTML 元素的样式。若要将 Bootstrap 添加到项目中,我在 ConventionsAndConstraints 项目中单击鼠标右键,在弹出菜单中选择【添加】➤【添加客户端库】,并将 twitter-bootstrap 添加至项目中。最终生成的 libman.json 配置文件代码清单31-5所示:

清单 31-5:ConventionsAndConstraints 文件夹下的 libman.json 文件的内容

{
  "version": "1.0",
  "defaultProvider": "cdnjs",
  "libraries": [
    {
      "library": "twitter-bootstrap@4.1.3",
      "destination": "wwwroot/lib/twitter-bootstrap/"
    }
  ]
}

最后的准备工作是在 Views 文件夹中创建 _ViewImports.cshtml 文件,该文件设置内置标签助手,以便在 Razor 视图中使用,并导入模型命名空间,如清单31-6所示。

清单 31-6:Views 文件夹下的 _ViewImports.cshtml 文件的内容

@using ConventionsAndConstraints.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

如果启动应用程序,您将看到如图31-1所示的结果。

图31-1 运行示例应用程序

使用应用程序模型和模型约定

MVC 支持约定而不是配置,这就是为什么您可以简单地创建一个类,其名称以Controller结尾,并开始定义 action 方法。在运行时,MVC 使用一个发现过程来定位应用程序中的所有控制器和 action,并检查它们是否使用诸如过滤器之类的特性。

发现过程的最终结果是应用程序模型,该模型由描述每个控制器类、action 方法和已找到的参数的对象组成。MVC 所依赖的约定在构造时应用于应用程序模型。例如,当发现控制器类时,使用类的名称作为在模型中表示它的控制器的基础;换句话说,HomeController类用于创建 Home 控制器。当路由系统标识必须由 Home 控制器处理的请求时,是应用程序模型提供到HomeController类的映射。

可以使用模型约定对应用程序模型进行自定义,模型约定是检查应用程序模型的内容并进行调整的类,例如合成新 action 或更改类用于创建控制器的方式。在下面的部分中,我将解释如何构造应用程序模型,介绍不同类型的模型约定,并演示可以使用约定的方式。表31-2为应用程序模型和模型约定简历。

表 31-2:应用程序模型和模型约定简历

问题 回答
它们是什么? 应用程序模型是对应用程序中发现的控制器和 action 的完整描述。模型约定允许将自定义更改应用于应用程序模型。
它们有何用途? 模型约定很有用,因为它们允许更改类和方法映射到控制器和 action 的方式。还可以执行其他定制,例如限制 action 接受的 HTTP 方法或应用 action 约束(本章后面将对此进行描述)。
如何使用它们? 模型约定是使用一系列接口定义的,这些接口将在下面的部分中描述,并作为特性应用或在Startup类中配置。
是否有任何缺陷或限制? 如以下各节所述,应用模型约定的方式有一些奇怪之处。
有没有其他选择? 没有,虽然您可以引入自己的组件来创建一个定制的应用程序模型,如果默认的应用程序模型不适合您的需要。

理解应用程序模型

在发现过程中,MVC 创建ApplicationModel类的一个实例,并将它找到的控制器和 action 的细节填充进去。当发现过程完成时,将应用模型约定进行您指定的任何自定义更改。理解应用程序模型的起点是检查Microsoft.AspNetCore.Mvc.ApplicationModels.ApplicationModel类定义的属性。如表31-3所述。

表 31-3:选定的 ApplicationModel 属性

名称 描述
Controllers 此属性返回一个IList<ControllerModel>,它包含了应用程序中的所有控制器
Filters 此属性返回一个IList<IFilterMetadata>,它包含了应用程序中的全局过滤器

注意:这似乎是一个枯燥的开始,特别是如果您想开始深入了解细节,但值得花点时间来了解一下本节中描述的类如何完整地描述了 MVC 应用程序的核心部分。了解应用程序模型是如何工作的,将帮助您理解更高级的特性是如何在幕后工作的,这将更好地帮助您在自己的项目中获得意想不到的结果时诊断问题。

本章的重要属性是Controllers,它返回一个列表,其中包含应用程序中发现的每个控制器的一个ControllerModel对象。表31-4描述了最重要的ControllerModel属性。

表 31-4:选定的 ControllerModel 属性

名称 描述
ControllerName 此字符串属性定义了控制器的名称。这是将用于匹配控制器路由段的名称。
ControllerType TypeInfo定义了控制器类的类型
ControllerProperties 此属性返回一个IList<PropertyModel>,它描述了控制器定义的所有属性,如表31-5所述。
Actions 此属性返回一个IList<ActionModel>,它描述了控制器定义的所有 action,如表31-6所述。
Filters 此属性返回一个IList<IFilterMetadata>,其中包含应用于控制器中所有 action 的过滤器。
RouteConstraints 此属性返回一个IList<IRouteConstraintProvider>,用于限制由控制器定义的路由目标 action 的方式。
Selectors 该属性返回一个IList<SelectorModel>,它包含 action 约束(在《使用 action 约束》一节中描述)和通过特性应用到控制器的路由信息的详细信息,如第15章所述。

您可以看到 MVC 的一些核心功能是如何被应用程序模型类捕获的。例如,ControllerName属性用于设置路由系统将用于匹配 URL 的名称,而ControllerType属性用于设置与名称相关的控制器类。

ControllerProperties属性返回PropertyModel对象的列表,每个对象描述由控制器定义的属性。表31-5描述了最重要的PropertyModel属性。

表 31-5:选定的 PropertyModel 属性

名称 描述
PropertyName 此字符串属性返回属性的名称
Attributes 此属性返回已应用于该属性的特性列表

Actions 属性返回一个ActionModel对象列表,每个对象都描述一个由单个控制器类定义的 action 方法。表31-6描述了ActionModel类最重要的属性。

表 31-6:选定的 ActionModel 属性

名称 描述
ActionName 此字符串属性定义 action 的名称,该名称将用于匹配 action 路由段。
ActionMethod MethodInfo属性用于指定实现该 action 的方法
Controller 此属性返回描述此 action 所属控制器的ControllerModel
Filters 此属性返回一个IList<IFilterMetadata>,其中包含应用于该 action 的筛选器。
Parameters 此属性返回一个IList<PropertyModel>,其中包含 action 方法所需参数的描述。
RouteConstraints 此属性返回一个IList<IRouteConstraintProvider>,用于限制路由如何针对该 action。
Selectors 该属性返回一个IList<SelectorModel>,它包含 action 约束(在《使用 action 约束》一节中描述)和通过特性应用到控制器的路由信息的详细信息,如第15章所述。

最后一级的详细信息是通过Parameters属性访问的,它返回一个ParameterModel对象列表,这些对象描述由 action 方法定义的每个参数。表31-7描述了ParameterModel类最重要的属性。

表 31-7:选定的 ParameterModel 属性

名称 描述
ParameterName 此字符串属性用于参数的名称
ParameterInfo PropertyInfo属性用于指定参数
BindingInfo BindingInfo属性用于配置模型绑定过程,如第27章所述。

这些类型 —— ApplicationModelControllerModelPropertyModelActionModelParameterModel —— 用于描述应用程序中控制器类的各个方面,以及它们的方法、属性和每个方法定义的参数。

定制应用程序模型

MVC 有一些内置的约定,当它用ControllerModelPropertyModelActionModelParameterModel对象填充ApplicationModel来描述它发现的控制器时,会使用这些约定。

有些约定是显式的,例如从控制器类的名称中删除Controller,并使用它来设置能ControllerModel对象的ControllerName属性。正是这种约定意味着您定义了一个类,比如HomeController,是以包含 Home 的 URL 段为目标。

其他约定是隐式的,例如每个类用于创建一个控制器,每个方法用于创建一个 action。大多数 MVC 开发人员认为这些约定是理所当然的,没有给它们任何有意识的考虑,但是应用程序模型的每个方面都可以改变。

在前几章中,我描述了改变 MVC 工作方式的特性,这些实际上是模型约定。表31-8描述了这些特性。

表 31-8:更改默认应用程序约定的基本特性

名称 描述
ActionName 此特性允许显式指定ActionModelActionName属性的值,而不是从方法名称派生。
NonController 此特性防止类用于创建ControllerModel对象。
NonAction 此特性防止使用方法创建ActionModel对象。

在清单31-7中,我使用ActionName特性来更改为表示HomeController类中的List方法而创建的 action 的名称。

清单 31-7:Controllers 文件夹下的 HomeController.cs 文件,定制应用程序模型

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;

namespace ConventionsAndConstraints.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionName("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

我已指定应使用名称Details来创建 action,以替换List的默认名称。您可以通过启动应用程序并请求 /Home/Details URL 来看到效果。如图31-2所示,请求由List方法处理。

图31-2 定制应用程序模型

理解模型约定的角色

表31-8中描述的特性允许对应用程序模型进行基本更改,但其范围有限。为了进行更多的定制,需要使用模型约定(也称为约定)。

表31-8中的特性允许在创建应用程序模型对象之前指定对它们的更改,例如重写用于 action 的名称。相反,创建模型约定允许您在创建模型对象之后更改模型对象,从而更改应用程序模型,然后允许应用更广泛的更改。有四种模型约定,每一种都由不同的接口定义,如表31-9所述。

表 31-9:应用程序模型约定接口

名称 描述
IApplicationModelConvention 此接口用于将约定应用于ApplicationModel对象
IControllerModelConvention 此接口用于对应用程序模型中的ControllerModel对象应用约定
IActionModelConvention 此接口用于对应用程序模型中的ActionModel对象应用约定。
IParameterModelConvention 此接口用于对应用程序模型中的ParameterModel对象应用约定。

所有四个接口都以相同的方式工作,只有在应用程序模型中操作的级别才会发生变化。例如,下面是IControllerModelConvention接口的定义:

namespace Microsoft.AspNetCore.Mvc.ApplicationModels 
{
    public interface IControllerModelConvention 
    {
        void Apply(ControllerModel controller);
    }
}

调用Apply方法是为了使模型约定有机会对其应用到的ControllerModel进行更改,它被作为方法参数接收。其他接口还定义了Apply方法,并且每个接口都接收它修改的类型的模型对象,因此IActionModelConvention接口接收ActionModel对象,IParameterModelConvention接口接收ParameterModel对象。

创建模型约定

控制器、action 和参数模型约定可以作为特性应用,这使得很容易设置它们应用的更改的范围。作为一个演示,我创建了一个 Infrastructure 文件夹,并向其添加了一个名为 ActionNamePrefixAttribute.cs 的类文件,用于定义清单31-8中所示的类。

清单 31-8:Infrastructure 文件夹下的 ActionNamePrefixAttribute.cs 文件的内容

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace ConventionsAndConstraints.Infrastructure
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
    public class ActionNamePrefixAttribute : Attribute, IActionModelConvention
    {
        private string namePrefix;

        public ActionNamePrefixAttribute(string prefix)
        {
            namePrefix = prefix;
        }

        public void Apply(ActionModel action)
        {
            action.ActionName = namePrefix + action.ActionName;
        }
    }
}

ActionNamePrefixAttribute类是从Attribute派生的,并实现了IActionModelConvention接口。它的构造函数接受一个用作前缀的字符串,该字符串是通过修改由Apply方法接收的ActionModel对象的ActionName属性来应用的。

提示:请注意,我已经限制了ActionNamePrefix特性的使用,以便它只能应用于方法。当将模型约定作为特性应用时,控制器约定仅在应用于类时才生效,action 约定仅在应用于方法时才有效,而参数约定仅在应用于参数时才有效。在错误级别上应用的约定将被忽略,不会出现任何错误。为了避免混淆,请使用AttributeUsage限制您创建的特性的范围。

在清单31-9中,我将模型约定特性应用于 Home 控制器的 action 方法之一。

清单 31-9:Controllers 文件夹下的 HomeController.cs 文件,应用模型约定

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionNamePrefix("Do")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

当 MVC 完成其发现过程时,它将创建一个ActionModel对象来描述List方法,检测ActionNamePrefix,并调用它的Apply方法。您可以通过运行应用程序并请求 /Home/DoList URL 来看到效果,它已经替换了默认约定下的List方法的 URL,如图31-3所示。

图31-3 应用模型约定

使用添加或删除模型的约定

应用模型约定的方式有一个怪癖,它阻止它们在应用程序模型中添加或删除对象。例如,假设您想要创建一个约定,可以通过两个不同的 actions 来实现某些方法。为了演示这个问题,我在 Infrastructure 文件夹中添加了一个名为 AddActionAttribute.cs 的类文件,并使用它来定义清单31-10所示的类。

清单 31-10:Infrastructure 文件夹下的 AddActionAttribute.cs 文件的内容

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;

namespace ConventionsAndConstraints.Infrastructure
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class AddActionAttribute : Attribute, IActionModelConvention
    {
        private string additionalName;
        public AddActionAttribute(string name)
        {
            additionalName = name;
        }

        public void Apply(ActionModel action)
        {
            action.Controller.Actions.Add(new ActionModel(action)
            {
                ActionName = additionalName
            });
        }
    }
}

此 action 模型约定使用一个ActionModel构造函数,复制现有对象的设置,然后更改新实例的ActionName属性。通过在ActionModel.Controller属性中导航,将新的ActionModel添加到控制器的 action 集合中。在清单31-11中,您可以看到我如何将模型约定应用于 Home 控制器。

清单 31-11:Controllers 文件夹下的 HomeController.cs 文件,应用模型约定

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

启动应用程序时,MVC 将开始其发现过程并报告以下错误:

InvalidOperationException: Collection was modified; enumeration operation may not execute.

当发现过程枚举 action 模型对象时,模型约定试图更改这些对象集,这会导致异常。避免错误需要一种不同的方法,如清单31-12所示。

清单 31-12:Infrastructure 文件夹下的 AddActionAttribute.cs 文件,创建一个安全约定

using System;
using Microsoft.AspNetCore.Mvc.ApplicationModels;
using System.Linq;

namespace ConventionsAndConstraints.Infrastructure
{
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    public class AddActionAttribute : Attribute
    {
        public string AdditionalName { get; }
        public AddActionAttribute(string name)
        {
            AdditionalName = name;
        }
    }

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    public class AdditionalActionsAttribute : Attribute,
        IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            var actions = controller.Actions
            .Select(a => new {
                Action = a,
                Names = a.Attributes.Select(attr =>
                    (attr as AddActionAttribute)?.AdditionalName)
            });
            foreach (var item in actions.ToList())
            {
                foreach (string name in item.Names)
                {
                    controller.Actions.Add(new ActionModel(item.Action)
                    {
                        ActionName = name
                    });
                }
            }
        }
    }
}

在 action 模型约定中不可能修改与控制器关联的 action 集,但我仍然需要某种方式来表示所需的更改。出于这个原因,我已经将AddActionAttribute类变成了一个特性,而不是一个模型约定。

可以在控制器模型约定中更改 action 集,这就是为什么我创建了AdditionalActionsAttribute类。Apply方法使用 LINQ 定位应用AddActionAttribute类的方法,并使用指定的名称创建新的ActionModel对象。

这个类最重要的部分是对应用于 LINQ 结果的ToList方法的调用。

...
foreach (var item in actions.ToList()) {
...

该方法强制对 LINQ 查询进行计算,并将结果放入一个新集合中,这意味着foreach循环在应用模型约定时枚举的对象集合与 MVC 枚举的对象集合不同。如果没有ToList调用,我将收到与清单31-12中的模型约定相同的错误消息;使用ToList调用,我能够创建新的 action 模型对象。清单31-13显示了如何将修改后的属性应用于 Home 控制器。

清单 31-13:Controllers 文件夹下的 HomeController.cs 文件,应用修正后的约定

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    [AdditionalActions]
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

通过启动应用程序并请求 /Home/Details 和 /Home/List URLs,您可以看到修订后的模型约定的效果。如图31-4所示,模型约定添加了一个由List方法处理的新 action,补充了默认创建的 action 模型。

图31-4 创建 action 模型的效果

理解模型约定执行顺序

模型约定按特定顺序应用,从最广泛的范围开始:首先应用控制器模型约定,然后是 action 模型约定,最后是参数模型约定。为了演示顺序,我已经将我在前面的示例中创建的两个自定义约定应用到HomeController类的List方法中,如清单31-14所示。

清单 31-14:Controllers 文件夹下的 HomeController.cs 文件,应用多个约定

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    [AdditionalActions]
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionNamePrefix("Do")]
        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

AdditionalActions属性是控制器模型约定,它首先应用并创建一个名为Details的新 action。接下来应用 action 模型约定ActionNamePrefix特性。它将Do前缀应用于与该方法关联的所有 action。结果是,List方法实现了两个 action,即DoListDoDetails,它们可以通过 /Home/DoList 和 /Home/DoDetails URLs 实现,如图31-5所示。

图31-5 创建 action 模型约定执行次序的效果

创建全局模型约定

如果您需要更改默认模型约定,则对于应用程序中的每个控制器、action 或参数,您可能都必须这样做。如果是这样的话,那么您可以创建一个全局模型约定,而不必记住将特性一致地应用于每个控制器类。全局模型约定是在Startup类中配置的,如清单31-15所示。

清单 31-15:ConventionsAndConstraints 文件夹下的 Startup.cs 文件,创建全局过滤器

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddMvcOptions(options => {
                options.Conventions.Add(new ActionNamePrefixAttribute("Do"));
                options.Conventions.Add(new AdditionalActionsAttribute());
            });
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

AddMvcOptions扩展方法接收到的MvcOptions对象定义了一个Conventions属性。此属性返回可以添加模型约定对象的列表集合。清单中全局地应用了两种自定义模型约定,这意味着所有 action 名称都将以Do作为前缀,所有方法都将检查AddAction特性。由于这些模型约定是全局应用,我在HomeController中移除了特性,如清单31-16所示。

清单 31-16:Controllers 文件夹下的 HomeController.cs 文件,移除模型约定

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    //[AdditionalActions]
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        //[ActionNamePrefix("Do")]
        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

全局模型约定在直接应用于类上的约定之前被应用。如果存在多个全局约定,则它们按注册的顺序被应用,而不考虑它们的类型。我在控制器模型约定之前注册了 action 模型约定,这意味着通过AddAction特性指定的Details action 是在ActionNamePrefixAttribute约定应用到所有 action 名称之后创建的。结果是List方法实现了两个 action,即DoListDetails,它们可以通过 /Home/DoList 和 /Home/Details URLs 实现,如图31-6所示。

图31-6 全局模型约定排序的效果

使用 Action 约束

Action 约束决定 action 方法是否适合处理特定请求,这可能导致您认为 action 约束与我在第19章中描述的授权过滤器类似。

事实上,action 约束的使用要有限得多。当 MVC 接收到一个 HTTP 请求时,它会经过一个选择过程来识别将用于处理它的 action 方法。如果有多个 action 可以处理请求,那么 MVC 需要某种方式来决定使用哪一个 action,这就是使用 action 约束的地方。表31-10为 action 约束简历。

表 31-10:Action 约束简历

问题 回答
它们是什么? action 约束是 MVC 使用的类,用于确定请求是否可以由特定的 action 处理。
它们有何用途? 如果有两个或多个 action 可以处理一个请求,那么 MVC 需要一些方法来决定哪一个是最合适的。action 约束用于提供该信息。
如何使用它们? 操作约束作为特性应用,这允许在整个应用程序中重用它们,这意味着决定一个 action 是否应该处理请求的逻辑不必在 action 方法本身内定义。
是否有任何缺陷或限制? action 约束可能应用得太广,并阻止任何适当的 action 方法处理请求,从而导致向客户端发送无帮助的【404 – Not Found】响应。
有没有其他选择? 如果希望在特定情况下限制对 action 的访问,则过滤器更有用,因为可以重定向客户端以显示有用的错误页。

准备示例项目

action 约束的作用是帮助 MVC 在两个或多个类似的都可以用于处理某一请求的 action 方法之间进行选择。这就是我在清单31-17中通过向 Home 控制器添加一个新的 action 方法而创建的情况。

清单 31-17:Controllers 文件夹下的 HomeController.cs 文件,创建两个适当的 Actions

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    //[AdditionalActions]
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionName("Index")]
        public IActionResult Other() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Other)
        });

        //[ActionNamePrefix("Do")]
        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

我添加了一个名为Other的新方法,并应用了ActionName特性,以便它生成一个名为Index的操作。我还更新了Startup类,以从本章的前一部分删除全局模型约定,如清单31-18所示。

清单 31-18:ConventionsAndConstraints 文件夹下的 Startup.cs 文件,移除约定

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc().AddMvcOptions(options => {
                //options.Conventions.Add(new ActionNamePrefixAttribute("Do"));
                //options.Conventions.Add(new AdditionalActionsAttribute());
            });
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

这意味着在 Home 控制器上有两个名为Index的 action,如果启动应用程序,您将看到图31-7所示的错误,这表明 MVC 不知道应该使用哪个 action。

图31-7 创造两种同样合适的 action 方法的效果

屏幕截图中的错误可能难于阅读,以下是消息的相关部分:

AmbiguousActionException: Multiple actions matched. The following actions matched route
data and had all constraints satisfied:
ConventionsAndConstraints.Controllers.HomeController.Index
ConventionsAndConstraints.Controllers.HomeController.Other

理解 action 约束

action 约束用于告诉 MVC 一个 action 方法是否可以用来处理请求,它实现了IActionConstraint接口,定义如下:

namespace Microsoft.AspNetCore.Mvc.ActionConstraints 
{
    public interface IActionConstraint : IActionConstraintMetadata 
    {
        int Order { get; }
        bool Accept(ActionConstraintContext context);
    }
}

当 MVC 完成选择一个 action 方法来处理请求的过程时,它会检查是否存在与它相关的约束。如果存在,则根据Order属性的值按顺序排列它们,然后依次调用每个属性的Accept方法。如果任何约束从Accept方法返回false,则 MVC 知道 action 方法不能用于处理当前请求。

提示IActionConstraint接口是从IActionConstraintMetadata派生的,IActionConstraintMetadata是一个不定义任何成员的接口。它不是直接使用的,当您定义自定义约束时,应该始终使用IActionConstaint接口,或者如果您想要创建一个具有要解析的依赖项的约束,则应该使用IActionConstraintFactory接口,如《在 action 约束中解决依赖关系》一节中所描述的那样。

为了帮助 action 约束做确定,MVC 为它们提供了用于 context 数据的ActionConstraintContext类的实例,该实例定义了表31-11中描述的属性。

表 31-11:ActionConstraintContext 属性

名称 描述
Candidates 此属性返回ActionSelectorCandidate对象列表,这些对象描述 MVC 为处理当前请求而列出的一组 action 方法。
CurrentCandidate 此属性返回ActionSelectorCandidate对象,该对象描述要求约束评估的 action 方法。
RouteContext 此属性返回一个RouteContext对象,它提供了有关路由数据的信息(通过RouteData属性)以及 HTTP 请求(通过HttpContext属性)

创建一个 Action 约束

最常见的约束类型检查请求,以确保满足某些策略,例如存在特定的 HTTP header 值。为了演示如何创建这种 action 约束,我在示例项目的 Infrastructure 文件夹中添加了一个名为 UserAgentAttribute.cs 的类文件,并使用它来定义清单31-19所示的类。

清单 31-19:Infrastructure 文件夹下的 UserAgentAttribute.cs 文件的内容

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ActionConstraints;

namespace ConventionsAndConstraints.Infrastructure
{
    public class UserAgentAttribute : Attribute, IActionConstraint
    {
        private string substring;

        public UserAgentAttribute(string sub)
        {
            substring = sub.ToLower();
        }

        public int Order { get; set; } = 0;

        public bool Accept(ActionConstraintContext context)
        {
            return context.RouteContext.HttpContext
                .Request.Headers["User-Agent"]
                .Any(h => h.ToLower().Contains(substring));
        }
    }
}

这是一个 action 约束特性,当User-Agent header不包含指定的字符串时,该属性将阻止请求匹配操作。在Accept方法中,我从HttpContext对象获取 HTTP header,并使用 LINQ 查看其中是否包含通过构造函数接收的子字符串。

注意:在真实项目中不要依赖于User-Agent header 来标识浏览器,因为 header 值常常具有误导性。例如,在我编写这篇文章时,微软 Edge 浏览器当时的版本发送了一个包含 Android、Apple、Chrome 和 Safari 的User-Agent header,这使得它很容易被误认为是另一种浏览器。一种更健壮的方法是使用 Javascript 库,如 Modernizr http://modernizr.com 来检测应用程序所依赖的功能。

在清单31-20中,我已经将约束应用于HomeController类中的一个方法。

清单 31-20:Controllers 文件夹下的 HomeController.cs 文件,应用 Action 约束

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    //[AdditionalActions]
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionName("Index")]
        [UserAgent("Edge")]
        public IActionResult Other() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Other)
        });

        //[ActionNamePrefix("Do")]
        [AddAction("Details")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

我将特性应用于Other方法,并指定不允许该 action 接收其User-Agent header 不包含术语Edge的请求。

如果启动应用程序并使用 Google Chrome 以及 Microsoft Edge 请求 /Home/Index URL,你将看到请求被不同的方法处理,如图31-8所示。

图31-8 action 约束的效果

理解约束对 action 选择的影响

前面的示例揭示了使用可能不是立即显而易见的约束的一个方面:对于请求,具有Accept方法返回true的约束的 action 优先于未对其应用约束的 action。

Home 控制器定义了两个Index action —— 由IndexOther方法创建 —— 它们都可以用于处理User-Agent header 包含字符串Edge的请求。使用另一种方法处理来自 Edge 浏览器的请求的原因是,它有一个应用于它的约束,而该约束的Accept方法返回true


创建可对比 action 约束

通过ActionConstraintContext对象的CandidatesCurrentCandidate属性,约束提供了处理请求的其他候选 action 的详细信息。每个潜在匹配都使用ActionSelectorCandidate类的实例来描述,它定义了表31-12中所示的属性。

表 31-12:ActionSelectorCandidate 属性

名称 描述
Action 此属性返回一个描述候选 action 的ActionDescriptor对象
Constraints 此属性返回包含应用于候选 action 的一组约束的IActionConstraint对象列表。

ActionDescriptor类用于通过表31-13中的属性来描述 action,其中许多属性与其他 context 对象提供的属性相似。

名称 描述
Name 此属性返回 action的名称
RouteConstraints 此属性返回一个IList<IRouteConstraintProvider>,用于限制路由如何以 action 为目标。
Parameters 此属性返回一个IList<PropertyModel>,它包含 action 方法所需参数的说明。
ActionConstraints 此属性返回一个IList<IActionConstraintMetadata>,它包含 action 的约束

约束可以检查候选 action,并深入了解它们是如何以及在何处应用的,可以用来微调它们的工作方式。作为一个例子,参考我对清单31-21中的 Home 控制器应用约束的方式。

清单 31-21:Controllers 文件夹下的 HomeController.cs 文件,应用约束

using ConventionsAndConstraints.Models;
using Microsoft.AspNetCore.Mvc;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints.Controllers
{
    public class HomeController : Controller
    {
        public IActionResult Index() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Index)
        });

        [ActionName("Index")]
        [UserAgent("Edge")]
        public IActionResult Other() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(Other)
        });

        [UserAgent("Edge")]
        public IActionResult List() => View("Result", new Result
        {
            Controller = nameof(HomeController),
            Action = nameof(List)
        });
    }
}

应用程序中只有一个List action,对其应用约束意味着只有其User-Agent header 包含Edge的请求才能使用它。例如,如果您使用 Chrome 发出请求,将收到【 404 – Not Found】的响应。

这没有任何帮助,因为用户无法理解为何它们收到错误,也没有任何解释性文字建议用户使用不同的浏览器。当您想要引导处理请求的 action 方法的选择时,action 约束是有用的,而不是当您想要防止使用特定的 action 时;如果这是您的目标,那么使用过滤器将允许您将客户端重定向到描述性错误页面,这是一个非常有用的响应。

为了解决这个问题,我更新了UserAgentAttribute类,以便当只有一个候选 action 可以处理请求时,约束不会拒绝请求,如清单31-22所示。

清单 31-22:Infrastructure 文件夹下的 UserAgentAttribute.cs 文件,检查其它候选

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ActionConstraints;

namespace ConventionsAndConstraints.Infrastructure
{
    public class UserAgentAttribute : Attribute, IActionConstraint
    {
        private string substring;

        public UserAgentAttribute(string sub)
        {
            substring = sub.ToLower();
        }

        public int Order { get; set; } = 0;

        public bool Accept(ActionConstraintContext context)
        {
            return context.RouteContext.HttpContext
                    .Request.Headers["User-Agent"]
                    .Any(h => h.ToLower().Contains(substring))
                || context.Candidates.Count() == 1;
        }
    }
}

附加的 LINQ 查询检查由CurrentCandidate属性返回的候选 action 是否是Candidates属性返回的集合中的唯一 action。如果是的话,那么约束就知道 MVC 没有可供选择的 action,并允许请求。通过启动应用程序并使用 Google Chrome 请求 /Home/List URL,您可以看到效果。即使 Chrome 发送的User-Agent header 不包含由List方法上的属性指定的术语Edge,但约束类确定没有其他候选项,并允许请求继续进行。

解析 action 约束中的依赖项

当您需要通过服务提供者解析 action 约束的依赖项时,将使用IActionConstraintFactory接口,我在第18章中对此进行了描述。下面是接口的定义:

using System;

namespace Microsoft.AspNetCore.Mvc.ActionConstraints 
{
    public interface IActionConstraintFactory : IActionConstraintMetadata 
    {
        IActionConstraint CreateInstance(IServiceProvider services);
        bool IsReusable { get; }
    }
}

调用CreateInstance方法来创建 action 约束类的新实例,并使用IsReusable属性来指示CreateInstance方法返回的对象是否可以用于多个请求。

为了演示这个接口的使用,我需要一个依赖项。为此,我在 Infrastructure 文件夹中添加了一个名为 UserAgentComparer.cs 的类文件,并使用它来定义清单31-23所示的类。

清单 31-23:Infrastructure 文件夹下的 UserAgentComparer.cs 文件的内容

using System.Linq;
using Microsoft.AspNetCore.Http;

namespace ConventionsAndConstraints.Infrastructure
{
    public class UserAgentComparer
    {
        public bool ContainsString(HttpRequest request, string agent)
        {
            string searchTerm = agent.ToLower();
            return request.Headers["User-Agent"]
                .Any(h => h.ToLower().Contains(searchTerm));
        }
    }
}

UserAgentComparer类定义了单个方法,该方法在 HTTP 请求的User-Agent header中查找字符串。这与我之前使用的功能相同,但是打包成一个单独的类,这样我就可以使用服务提供者来管理它的生命周期,我在清单31-24中已经配置了它。

清单 31-24:ConventionsAndConstraints 文件夹下的 Startup.cs 文件,注册一个类型

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using ConventionsAndConstraints.Infrastructure;

namespace ConventionsAndConstraints
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<UserAgentComparer>();
            services.AddMvc().AddMvcOptions(options => {
                //options.Conventions.Add(new ActionNamePrefixAttribute("Do"));
                //options.Conventions.Add(new AdditionalActionsAttribute());
            });
        }
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            app.UseStatusCodePages();
            app.UseDeveloperExceptionPage();
            app.UseStaticFiles();
            app.UseMvcWithDefaultRoute();
        }
    }
}

我选择了单例生命周期,这意味着将使用UserAgentComparer的单个实例。在清单31-25中,我更新了UserAgent约束,以便它将 header 的检查委托给UserAgentComparer对象,该对象是通过服务提供者获得的。

清单 31-25:Infrastructure 文件夹中 UserAgentAttribute.cs,解析依赖项

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc.ActionConstraints;
using Microsoft.Extensions.DependencyInjection;

namespace ConventionsAndConstraints.Infrastructure
{
    public class UserAgentAttribute : Attribute, IActionConstraintFactory
    {
        private string substring;

        public UserAgentAttribute(string sub)
        {
            substring = sub;
        }

        public IActionConstraint CreateInstance(IServiceProvider services)
        {
            return new UserAgentConstraint(services.GetService<UserAgentComparer>(),
            substring);
        }

        public bool IsReusable => false;

        private class UserAgentConstraint : IActionConstraint
        {
            private UserAgentComparer comparer;
            private string substring;

            public UserAgentConstraint(UserAgentComparer comp, string sub)
            {
                comparer = comp;
                substring = sub.ToLower();
            }

            public int Order { get; set; } = 0;

            public bool Accept(ActionConstraintContext context)
            {
                return comparer.ContainsString(context.RouteContext
                        .HttpContext.Request, substring)
                    || context.Candidates.Count() == 1;
            }
        }
    }
}

在此模型中,应用于 action 方法的属性负责在调用约束类CreateInstance方法时创建约束类的实例。CreateInstance方法的参数是IServiceProvider对象,我在示例中使用它获取UserAgentComparer,这样就可以创建私有约束类的实例,然后在选择过程中使用该实例。


避免范围陷阱

与其他基于特性的功能一样,将约束特性应用于控制器类等同于将该特性应用于每个单独的方法。然而,这通常会产生不良的结果,因为约束的目的是帮助 MVC 选择一个 action 方法,而这并不是将相同的约束应用于控制器中所有 action 的效果。

例如,如果我将UserAgent特性应用于HomeController类,那么任何浏览器都无法访问Index action。这两个Index action 同样适用于那些在User-Agent字符串中包含Edge的浏览器,这将导致异常。对于所有其他浏览器,这两个Index action 都不合适,这将导致【404 – Not Found】响应。

可以使用约束中的 context 对象查找其他约束,并查看它们是否可能拒绝请求,但是,这会导致对每个请求多次调用每个约束的Accept方法,这可能是一个昂贵的过程,也是应该避免的过程。

当有多个 action 方法可以处理相同的请求,然后将约束应用到这些方法时,约束工作得最好。


总结

在本章中,我描述了用于定制 MVC 操作方式的两个特性。我解释了如何使用模型约定来改变类和方法映射到控制器和 action 的方式。我还描述了如何使用 action 约束来限制 action 可能处理的请求的范围,以及如何使用它们从请求到达时标识的候选人列表中选择一个 action。

这就是我要教你的关于 ASP.NET Core MVC 2 的全部内容。我首先创建了一个简单的应用程序,然后对框架中的不同组件进行了全面的介绍,向您展示了如何配置、定制或完全替换它们。

我祝愿您在 MVC 项目中一切顺利,我只能希望你像我喜欢写这本书一样喜欢阅读这本书。

;

© 2018 - IOT小分队文章发布系统 v0.3